<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <script src="https://d3js.org/d3.v6.min.js"></script>
    {% load static %}
    <style>
        .node rect.actor {
            stroke: #000;
            stroke-width: 1.5px;
        }

        .node text {
            font: 10px sans-serif;
            pointer-events: none;
        }

        .link {
            stroke: #999;
            stroke-opacity: 0.6;
            stroke-width: 1.5px;
        }
        .arrowHead {
            fill: #555;
        }
    </style>
</head>
<body>
<script>
    Promise.all([
        d3.json("{% static 'Vis/data/actors.json' %}"),
        d3.json("{% static 'Vis/data/models.json' %}"),
        d3.json("{% static 'Vis/data/phases.json' %}"),
    ]).then(([actorsData, modelsData, phasesData]) => {
        createForceDirectedGraph(actorsData, modelsData, phasesData);
    });

    function createForceDirectedGraph(actorsData, modelsData, phasesData) {
        const width = 3000;
        const height = 5000;
        const actor_radius = 10;
        const model_radius = 5;
        const customColors = [
            "#1f77b4",
            "#ff7f0e",
            "#2ca02c",
            "#d62728",
            "#9467bd",
            "#8c564b",
            "#e377c2",
            "#7f7f7f",
            "#bcbd22",
            "#17becf",
            "#aec7e8",
            "#ffbb78",
            "#98df8a",
            "#ff9896",
        ];
        const colorScale = d3.scaleOrdinal(customColors);
        colorScale.domain(phasesData.map((d) => d.name));
        const svg = d3
            .select("body")
            .append("svg")
            .attr("width", width)
            .attr("height", height)


        const container = svg.append("g");

        svg.call(
            d3.zoom().on("zoom", function (event) {
                container.attr("transform", event.transform);
            })
        );

        const linksData = modelsData.flatMap((model) => {
            return model.produced_actors
                .map((actorName) => {
                    return { source: actorName, target: model.name };
                })
                .concat(
                    model.consumed_actors.map((actorName) => {
                        return { source: model.name, target: actorName };
                    })
                );
        });

        const simulation = d3
            .forceSimulation(actorsData.concat(modelsData))
            .force("link", d3.forceLink(linksData).id((d) => d.name).distance(60))
            .force("charge", d3.forceManyBody().strength(-250))
            .force("center", d3.forceCenter(width / 2, height / 2));

        const link = container
            .append("g")
            .attr("class", "links")
            .selectAll("line")
            .data(linksData)
            .enter()
            .append("line")
            .attr("class", "link")
            //.attr("stroke", "#555")
            .attr("stroke-width", 1)
            .attr("marker-end", "url(#arrow)");

        const nodeData = actorsData.concat(modelsData);

        const node = container
            .append("g")
            .attr("class", "nodes")
            .selectAll("g")
            .data(nodeData)
            .enter()
            .append("g")
            .attr("class", "node");

        node
            .append("circle")
            .attr("class", (d) => (d.phase_name ? "actor" : "model"))
            .attr("r", (d) => (d.phase_name ? actor_radius : model_radius))
            .attr("fill", (d) => (d.phase_name ? phaseColor(d.phase_name) : "lightgray"))
            .attr("opacity", 0.7);

        node
            .append("text")
            .attr("font-size", "8px") // 设置字体大小为 10px，您可以根据需要调整此值
            .text((d) => d.name)
            .each(function(d) {
                // 获取文本元素的边界框
                const bbox = this.getBBox();
                // 计算文本的宽度和高度
                const width = bbox.width;
                const height = bbox.height;
                // 设置 dx 为文本宽度的负一半
                d.dx = -width / 2;
                // 设置 dy 为文本高度的一半
                d.dy = height / 3;
            })
            .attr("dx", (d) => d.dx)
            .attr("dy", (d) => d.dy)
            .attr("opacity", 0.8)
        ;

        simulation.on("tick", () => {
            // 更新节点位置的边界
            nodeData.forEach((d) => {
                const radius = d.phase_name ? actor_radius : model_radius;
                d.x = Math.max(radius, Math.min(width - radius, d.x));
                d.y = Math.max(radius, Math.min(height - radius, d.y));
            });
            link
                .attr("x1", (d) => d.source.x)
                .attr("y1", (d) => d.source.y)
                .attr("x2", (d) => d.target.x)
                .attr("y2", (d) => d.target.y);

            node.attr("transform", (d) => `translate(${d.x}, ${d.y})`);
        });

        node.call(
            d3
                .drag()
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended)
        );

        function dragstarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }

        function dragged(event, d) {
            d.fx = event.x;
            d.fy = event.y;
        }

        function dragended(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        }

        function phaseColor(phaseName) {
            return colorScale(phaseName) || "gray";
        }

        // 定义箭头标记
        svg
            .append("defs")
            .selectAll("marker")
            .data(["arrow"])
            .enter()
            .append("marker")
            .attr("id", "arrow")
            .attr("viewBox", "0 -5 10 10")
            .attr("refX", 15)
            .attr("refY", 0)
            .attr("markerWidth", 4)
            .attr("markerHeight", 4)
            .attr("orient", "auto")
            .append("path")
            .attr("d", "M0,-5L10,0L0,5")
            .attr("class", "arrowHead");

        // 创建图例
        const legend = svg
            .append("g")
            .attr("class", "legend")
            .attr("transform", `translate(20, 20)`)
            .selectAll("g")
            .data(phasesData)
            .enter()
            .append("g")
            .attr("transform", (d, i) => `translate(0,${i * 20})`);

        legend
            .append("rect")
            .attr("width", 18)
            .attr("height", 18)
            .attr("fill", (d) => phaseColor(d.name));

        legend
            .append("text")
            .attr("x", 24)
            .attr("y", 9)
            .attr("dy", ".35em")
            .text((d) => d.name)
            .style("opacity", 0.8);

        phasesData.forEach((phase) => {
            phase.selected = false;
        });

        legend.on("click", (event, phaseData) => {
            // 切换所选阶段的选中状态
            phaseData.selected = !phaseData.selected;

            //如果所有的phase都未选择，将其改为所有的phase都选择
            if (phasesData.every((phase) => !phase.selected)) {
                phasesData.forEach((phase) => {
                    phase.selected = true;
                });
            }

            //更新图例的透明度
            legend.style("opacity", (d) => (d.selected ? 0.8 : 0.3));

            // 更新节点的显示状态
            node.style("display", (d) => {
                return "none";
            });

            // 更新箭头的显示状态
            link.style("display", (d) => {
                if (
                    (d.source.phase_name &&
                        phasesData.find((phase) => phase.name === d.source.phase_name).selected) ||
                    (d.target.phase_name &&
                        phasesData.find((phase) => phase.name === d.target.phase_name).selected)
                ) {
                    node.filter((e) => e.name === d.source.name).style("display", "block");
                    node.filter((e) => e.name === d.target.name).style("display", "block");
                    return "block";
                }
                return "none";
            });

            // 如果有所的phase都被选择，将其初始化为所有的phase都未选择
            if (phasesData.every((phase) => phase.selected)) {
                phasesData.forEach((phase) => {
                    phase.selected = false;
                });
            }
        });
    }
</script>
</body>
</html>